/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.bennedum.SimpleMCClient;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.JOptionPane;
import org.bennedum.SimpleMCClient.protocol.Packet;
import org.bennedum.SimpleMCClient.protocol.Packet01;
import org.bennedum.SimpleMCClient.protocol.Packet02;
import org.bennedum.SimpleMCClient.protocol.Packet03;
import org.bennedum.SimpleMCClient.protocol.Packet04;
import org.bennedum.SimpleMCClient.protocol.Packet0D;
import org.bennedum.SimpleMCClient.protocol.PacketException;
import org.bennedum.SimpleMCClient.protocol.PacketFF;

/**
 *
 * @author frdfsnlght <frdfsnlght@gmail.com>
 */
public class Client {

    private static final String AUTH_URL = "http://www.minecraft.net/game/joinserver.jsp";

    private static final long MOVEMENT_INTERVAL = 50;

    private InetSocketAddress serverAddress;
    private State state = State.NONE;
    private byte[] readBuffer = null;
    private final List<byte[]> writeBuffers = new ArrayList<byte[]>();
    private long lastMoveSent = 0;

    private long time = 0;

    private double x = 0;
    private double y = 64;
    private double z = 0;
    private float pitch = 0;
    private float yaw = 0;
    private double stance = 0;
    private boolean onGround = true;

    public Client(InetSocketAddress address) {
        serverAddress = address;
    }

    public InetSocketAddress getServerAddress() {
        return serverAddress;
    }

    public void connect() {
        Main.mainWindow = new MainWindow();
        Main.mainWindow.setVisible(true);
        state = State.OPENING;
        Main.network.connect(this);
    }

    public void disconnect() {
        Main.mainWindow.chatOut("Disconnecting...");
        state = State.CLOSING;
        Main.network.disconnect();
    }

    public void sendChat(String message) {
        Packet03 p = new Packet03(message);
        sendPacket(p);
    }

    public void sendPacket(Packet packet) {
        synchronized (writeBuffers) {
            writeBuffers.add(packet.getBuffer());
        }
        if (((state == State.ESTABLISHED) && (packet.getType() != 0x0D)) || (state != State.ESTABLISHED))
            Main.out("TX: %s", packet);
        Main.network.wantWrite();
    }

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
        Main.mainWindow.onPositionChanged(x, y, z, yaw, pitch);
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
        this.stance = y + 1.5;
        Main.mainWindow.onPositionChanged(x, y, z, yaw, pitch);
    }

    public double getZ() {
        return z;
    }

    public void setZ(double z) {
        this.z = z;
        Main.mainWindow.onPositionChanged(x, y, z, yaw, pitch);
    }

    public float getYaw() {
        return yaw;
    }

    public void setYaw(float yaw) {
        this.yaw = yaw;
        Main.mainWindow.onPositionChanged(x, y, z, yaw, pitch);
    }

    public float getPitch() {
        return pitch;
    }

    public void setPitch(float pitch) {
        this.pitch = pitch;
        Main.mainWindow.onPositionChanged(x, y, z, yaw, pitch);
    }


    // Network callbacks

    public void onException(Throwable t) {
        t.printStackTrace();
        disconnect();
    }

    public void onOpened() {
        Main.out("onOpened");
        state = State.HANDSHAKE;
        Packet p = new Packet02(Main.loginWindow.getAuthUser());
        sendPacket(p);
    }

    public void onClosed() {
        Main.out("onClosed");
        state = State.CLOSED;
        MainWindow w = Main.mainWindow;
        Main.mainWindow = null;
        w.setVisible(false);
        w.dispose();
        Main.client = null;
        java.awt.EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                Main.connect();
            }
        });
    }

    public void onKilled() {
        Main.out("onKilled");
    }

    public void onReadData(byte[] data) {
        if (readBuffer == null)
            readBuffer = data;
        else {
            int oldSize = readBuffer.length;
            readBuffer = Arrays.copyOf(readBuffer, oldSize + data.length);
            System.arraycopy(data, 0, readBuffer, oldSize, data.length);
        }

        try {
            Packet packet = Packet.decode(readBuffer);
            while (packet != null) {
                readBuffer = Arrays.copyOfRange(readBuffer, packet.getLength(), readBuffer.length);
                processPacket(packet);
                packet = Packet.decode(readBuffer);
            }
        } catch (PacketException e) {
            e.printStackTrace();
            disconnect();
        }
    }

    public boolean onHasWriteData() {
        synchronized (writeBuffers) {
            return ! writeBuffers.isEmpty();
        }
    }

    public byte[] onGetWriteData() {
        synchronized (writeBuffers) {
            if (writeBuffers.isEmpty()) return null;
            return writeBuffers.remove(0);
        }
    }

    public void onPutWriteData(byte[] data) {
        synchronized (writeBuffers) {
            writeBuffers.add(0, data);
        }
    }

    public void onWriteCompleted() {
    }

    public void onIdle() {
        if (state == State.ESTABLISHED)
            if ((System.currentTimeMillis() - lastMoveSent) >= MOVEMENT_INTERVAL) {
                lastMoveSent = System.currentTimeMillis();
                Packet p = new Packet0D(x, y, z, stance, yaw, pitch, onGround);
                sendPacket(p);
            }
    }

    private void processPacket(Packet packet) throws PacketException {
        switch (packet.getType()) {
            case 0x04:  // time update
                Packet04 p = (Packet04)packet;
                time = p.getTime() % 24000;
                Main.mainWindow.onTimeChanged(time);
                return;
            case 0x1F:  // entity relative move
                return;
            case 0x32:  // prechunk
                //System.out.print('c');
                return;
            case 0x33:  // chunk
                //System.out.print('C');
                return;
        }

        Main.out("RX %s", packet);

        switch (packet.getType()) {
            case 0x01:
                state = State.INIT;
                break;
            case 0x02:
                Packet02 hs = (Packet02)packet;
                if (state != State.HANDSHAKE)
                    throw new PacketException("unexpected handshake packet!");
                String hash = hs.getConnectionHash();
                if (hash.equals("-"))
                    startLogin();
                else if (hash.equals("+")) {
                    Main.out("need to authenticate +");
                    disconnect();
                } else
                    authenticate(hash);
                break;
            case 0x03:
                Main.mainWindow.chatOut(((Packet03)packet).getMessage());
                break;
            case 0x0D:
                Packet0D pal = (Packet0D)packet;
                setX(pal.getX());
                setY(pal.getStCY());
                setZ(pal.getZ());
                setYaw(pal.getYaw());
                setPitch(pal.getPitch());
                stance = pal.getStCStance();
                onGround = pal.getOnGround();
                Main.mainWindow.onPositionChanged(x, y, z, yaw, pitch);

                if (state == State.INIT) {
                    pal.setCtSY(y);
                    pal.setCtSStance(stance);
                    state = State.ESTABLISHED;
                    sendPacket(pal);
                    Main.out("Logged in!");
                }
                break;
            case 0xff:
                PacketFF kick = (PacketFF)packet;
                Main.out("KICKED: %s", kick.getReason());
                disconnect();
                break;
        }
    }

    private void authenticate(String serverHash) {
        try {
            Main.out("authenticating...");
            URL url = new URL(String.format("%s?user=%s&sessionId=%s&serverId=%s", AUTH_URL, Main.loginWindow.getAuthUser(), Main.loginWindow.getSessionId(), serverHash));
            HttpURLConnection conn = (HttpURLConnection)url.openConnection();
            conn.setDoInput(true);
            conn.setDoOutput(false);
            conn.setUseCaches(false);
            conn.setAllowUserInteraction(false);
            conn.setRequestMethod("GET");
            int status = conn.getResponseCode();
            if (status == 200) {
                BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                String res = reader.readLine();
                Main.out("auth result '%s'", res);
                if (res.equals("OK")) {
                    startLogin();
                    return;
                }
            }
            disconnect();
            JOptionPane.showMessageDialog(null, "Unable to authenticate!", "Login Failed", JOptionPane.ERROR_MESSAGE);
        } catch (IOException e) {
            JOptionPane.showMessageDialog(null, "IOException: " + e.getMessage(), "Login Failed", JOptionPane.ERROR_MESSAGE);
        }
    }

    private void startLogin() {
        state = State.LOGIN;
        Packet p = new Packet01(14, Main.loginWindow.getAuthUser());
        sendPacket(p);
    }


    private enum State {
        NONE,
        OPENING,
        HANDSHAKE,
        LOGIN,
        INIT,
        ESTABLISHED,
        CLOSING,
        CLOSED
    }

}
